from random import random
from math import acos, pi

def is_vec3_friendly(arg):
	if ((type(arg) is list) or (type(arg) is tuple) or 
		isinstance(arg, vec3)) and (len(arg) is 3):
		try:
			float(arg[0])
			float(arg[1])
			float(arg[2])
			return True
		except ValueError:
			return False
	else:
		return False


def is_vec2_friendly(arg):
	if ((type(arg) is list) or (type(arg) is tuple) or 
		isinstance(arg, vec2)) and (len(arg) is 2):
		try:
			float(arg[0])
			float(arg[1])
			return True
		except ValueError:
			return False
	else:
		return False

class vec3:
	"""A 3-vector, for use in coordinates and directions.  Please note that 
	all for binary operators to behave correctly, the vec3 must be on the left
	side of the operator:
	a = vec3((1, 2, 3)); b = (4, 5, 6)
	a + b => (5.0, 7.0, 9.0) as a vec3
	b + a => (4, 5, 6, 1.0, 2.0, 3.0) as a tuple
	"""
	def __init__(self, *args):
		if len(args) == 3:
			try:
				self.x = float(args[0])
				self.y = float(args[1])
				self.z = float(args[2])
			except ValueError, e:
				raise ValueError(str(e).split()[-1] + \
					  " was not convertible to float.")
		elif len(args) == 1:
			if is_vec3_friendly(args[0]):
				try:
					self.x = float(args[0][0])
					self.y = float(args[0][1])
					self.z = float(args[0][2])
				except ValueError, e:
					raise ValueError(str(e).split()[-1] + \
						  " was not convertible to float.")
			if isinstance(args[0], vec2):
				self.x = args[0].x
				self.y = args[0].y
				self.z = 0.0
		else:
			raise ValueError("Expected three floats or a 3-length " + \
			                 "vector-like entity")

	def __repr__(self):
		"""How the vec3 sees itself"""
		return "vec3(%g, %g, %g)" % (self.x, self.y, self.z)
	
	def __str__(self):
		"""How to represent the vector as a string"""
		return "(%g, %g, %g)" % (self.x, self.y, self.z)

	def __len__(self):
		"""The length of the vector (since it is more or less a tuple)"""
		return 3

	def __getitem__(self, key):
		"""Implements indexing with [], which is not really necessary"""
		if key == 0:
			return self.x
		if key == 1:
			return self.y
		if key == 2:
			return self.z
		else:
			if type(key) is not int:
				raise TypeError("to index with vec3[key], " + \
				                "key must be an integer")
			else:
				raise IndexError("key was out of bounds, i.e., not 0, 1, or 2")

	def __setitem__(self, key, value):
		"""Implements setting items with the [] indexing"""
		if key == 0:
			self.x = value
		if key == 1:
			self.y = value
		if key == 2:
			self.z = value
		else:
			if type(key) is not int:
				raise TypeError("To index with vec3[key], " + \
				                "key must be an integer")
			else:
				raise IndexError("key was out of bounds, i.e., not 0, 1, or 2")

	def __nonzero__(self):
		"""Implementation of whether a vec3 is True or False. Because a vec3
		uses floating point elements, this clamps vectors below a certain size
		to zero."""
		if (self * self) < 1e-12:
			return False
		else:
			return True

	def __abs__(self):
		"""Implementation of the absolute value function.  Returns a copy with
		all the elements positive."""
		if self.x < 0:
			x = -self.x
		else:
			x = self.x
		if self.y < 0:
			y = -self.y
		else:
			y = self.y
		if self.z < 0:
			z = -self.z
		else:
			z = self.z
		return vec3(x,y,z)
		

	def __neg__(self):
		"""Implementation of negation, i.e., the unary - operator"""
		return vec3(-self.x, -self.y, -self.z)

	def __add__(self, other):
		"""Implementation of the binary +.  Works with other vec3s, as well as
		lists and tuples of length 3."""
		if is_vec3_friendly(other):
			try:
				return vec3(self.x + float(other[0]), \
				            self.y + float(other[1]), \
				            self.z + float(other[2]))
			except ValueError:
				raise ValueError("the elements of the argument to " + \
				                 "vec3.__add__ must be convertible to float.")

		if (type(other) is list) or (type(other) is tuple):
			raise ValueError("Expected the argument of vec3.__add__ to " + \
			                 "have length 3, instead got %d" % len(other))
		else:
			raise TypeError("unsupported operand type for vec3 + " + \
		                    str(type(other)))

	def __sub__(self, other):
		"""Implementation of the binary -.  Works with other vec3s, as well as
		lists and tuples of length 3."""
		if is_vec3_friendly(other):
			try:
				return vec3(self.x - float(other[0]), \
				            self.y - float(other[1]), \
				            self.z - float(other[2]))
			except ValueError:
				raise ValueError("the elements of the argument to " + \
				                 "vec3.__sub__ must be convertible to float.")

		if (type(other) is list) or (type(other) is tuple):
			raise ValueError("Expected the argument of vec3.__sub__ to " + \
			                 "have length 3, instead got %d" % len(other))
		else:
			raise TypeError("unsupported operand type for vec3 - " + \
		                    str(type(other)))
		
	def __mul__(self, other):
		"""Implementation of the binary *.  Acts as a dot product between a
		vec3 and another vec3 or list or tuple of length 3.
		However, acts as an elementwise multiplication if the argument is 
		castable to a floating point."""
		if is_vec3_friendly(other):
			return (self.x * other[0] + self.y * other[1] + self.z * other[2])
		try:
			scalar = float(other)
			return vec3(self.x * scalar, \
			            self.y * scalar, \
			            self.z * scalar)
		except:
			pass
		raise TypeError("unsupported operand type for vec3 * " + \
		                str(type(other)))

	def __div__(self, other):
		"""Implementation of the binary /.  Acts as an elementwise division
		if the argument is castable to a floating point."""
		try:
			scalar = float(other)
			if abs(scalar) < 1e-9:
				raise ZeroDivisionError("vec2s may not be divided by zero")
			return vec3(self.x / scalar, \
			            self.y / scalar, \
			            self.z / scalar)
		except ZeroDivisionError, e:
			raise ZeroDivisionError(e)
		except:
			pass
		raise TypeError("unsupported operand type for vec3 / " + \
		                str(type(other)))

	def __lt__(self, other):
		"""Implementation of the binary < operator.  Returns True if this
		vector is shorter (has inferior magnitude) to another vec3, or if it
		has magnitude less than a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec3):
			return self.mag() < other.mag()
		try:
			return self.mag() < float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec3 < " + \
		                str(type(other)))

	def __le__(self, other):
		"""Implementation of the binary <= operator.  Returns True if this
		vector is shorter than or the same length as another vec3, or if it
		has magnitude less than or equal to a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec3):
			return self.mag() <= other.mag()
		try:
			return self.mag() <= float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec3 <= " + \
		                str(type(other)))

	def __gt__(self, other):
		"""Implementation of the binary > operator.  Returns True if this
		vector is longer (has greater magnitude) to another vec3, or if it
		has magnitude greater than a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec3):
			return self.mag() > other.mag()
		try:
			return self.mag() > float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec3 > " + \
		                str(type(other)))

	def __ge__(self, other):
		"""Implementation of the binary >= operator.  Returns True if this
		vector is at least as long as another vec3, or if it
		has magnitude greater than or equal to a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec3):
			return self.mag() >= other.mag()
		try:
			return self.mag() >= float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec3 >= " + \
		                str(type(other)))

	def d2(self):
		"""distance-squared, for use in mag and other shortcuttery"""
		return (self.x*self.x + self.y*self.y + self.z*self.z)

	def mag(self):
		"""Returns the magnitude of this vector, that is, the 2-norm."""
		return self.d2()**.5

	def normalize(self):
		"""Returns a copy of this vector that has been normalized, that is,
		a vec3 with the same direction, but magnitude 1.
		The exception to this is a vector of magnitude zero, which cannot be
		normalized.  Then it will return the zero vector."""
		if self:
			mag = self.mag()
			return vec3(self.x / mag, \
			            self.y / mag, \
			            self.z / mag)
		else:
			return vec3(0, 0, 0)

	def min(self, other):
		"""Given another vec3-friendly, returns the element-wise minimum of the
		two vectors."""
		if not is_vec3_friendly(other):
			raise ValueError("Expected argument to be vec3-friendly, " + \
			                 "instead got %s" % other)
		return vec3(min(self[0], other[0]), \
		            min(self[1], other[1]), \
		            min(self[2], other[2]))

	def max(self, other):
		"""Given another vec3-friendly, returns the element-wise maximum of the
		two vectors."""
		if not is_vec3_friendly(other):
			raise ValueError("Expected argument to be vec3-friendly, " + \
			                 "instead got %s" % other)
		return vec3(max(self[0], other[0]), \
		            max(self[1], other[1]), \
		            max(self[2], other[2]))

	def random(self):
		"""Returns a vector chosen at random from within the box (0,0,0), this
		vector. The randomness is a uniform distribution.
		TODO: consider adding other randomizations."""
		return vec3(self.x * random(), \
		            self.y * random(), \
		            self.z * random())

	def int(self):
		"""Returns a copy of the vector with its elements cast as ints.
		Gives up if it proves difficult on a particular element."""
		temp = vec3(self)
		for i in range(3):
			try:
				temp[i] = int(temp[i])
			except:
				pass
		return temp
	
	def float(self):
		"""Returns a copy of the vector with its elements cast as floats.
		Gives up if it proves difficult on a particular element."""
		temp = vec3(self)
		for i in range(3):
			try:
				temp[i] = float(temp[i])
			except:
				pass
		return temp

	def angle(self, other, mode="rad"):
		"""The angle between two vectors is the arccosine of their dot product
		devided by the product of their magnitudes."""
		if not is_vec3_friendly(other):
			raise ValueError("Expected argument to be vec3-friendly, " + \
			                 "instead got %s" % other)
		temp = vec3(other)
		if mode == "rad":
			return acos(self.normalize() * temp.normalize())
		if mode == "deg":
			return acos(self.normalize() * temp.normalize()) * 180 / pi
		else:
			raise ValueError("mode must be 'deg' or 'rad', got %s" % mode)




class vec2:
	"""A 2-vector, for use in coordinates and directions.  Please note that 
	all for binary operators to behave correctly, the vec2 must be on the left
	side of the operator:
	a = vec2((1, 2)); b = (4, 5)
	a + b => (5.0, 7.0) as a vec2
	b + a => (4, 5, 1.0, 2.0) as a tuple
	"""
	def __init__(self, *args):
		if len(args) == 2:
			try:
				self.x = float(args[0])
				self.y = float(args[1])
			except ValueError, e:
				raise ValueError(str(e).split()[-1] + \
					  " was not convertible to float.")
		elif len(args) == 1:
			if is_vec2_friendly(args[0]):
				try:
					self.x = float(args[0][0])
					self.y = float(args[0][1])
				except ValueError, e:
					raise ValueError(str(e).split()[-1] + \
						  " was not convertible to float.")
			if isinstance(args[0], vec3):
				self.x = args[0].x
				self.y = args[0].y
		else:
			raise ValueError("Expected two floats or a 2-length " + \
			                 "vector-like entity")

	def __repr__(self):
		"""How the vec2 sees itself"""
		return "vec2(%g, %g)" % (self.x, self.y)
	
	def __str__(self):
		"""How to represent the vector as a string"""
		return "(%g, %g)" % (self.x, self.y)

	def __len__(self):
		"""The length of the vector (since it is more or less a tuple)"""
		return 2

	def __getitem__(self, key):
		"""Implements indexing with [], which is not really necessary"""
		if key == 0:
			return self.x
		if key == 1:
			return self.y
		else:
			if type(key) is not int:
				raise TypeError("to index with vec2[key], " + \
				                "key must be an integer")
			else:
				raise IndexError("key was out of bounds, i.e., not 0 or 1")

	def __setitem__(self, key, value):
		"""Implements setting items with the [] indexing"""
		if key == 0:
			self.x = value
		if key == 1:
			self.y = value
		else:
			if type(key) is not int:
				raise TypeError("To index with vec2[key], " + \
				                "key must be an integer")
			else:
				raise IndexError("key was out of bounds, i.e., not 0 or 1")

	def __nonzero__(self):
		"""Implementation of whether a vec2 is True or False. Because a vec2
		uses floating point elements, this clamps vectors below a certain size
		to zero."""
		if (self * self) < 1e-12:
			return False
		else:
			return True

	def __abs__(self):
		"""Implementation of the absolute value function.  Returns a copy with
		all the elements positive."""
		if self.x < 0:
			x = -self.x
		else:
			x = self.x
		if self.y < 0:
			y = -self.y
		else:
			y = self.y
		return vec2(x,y)

	def __neg__(self):
		"""Implementation of negation, i.e., the unary - operator"""
		return vec2(-self.x, -self.y)

	def __add__(self, other):
		"""Implementation of the binary +.  Works with other vec2s, as well as
		lists and tuples of length 2."""
		if is_vec2_friendly(other):
			try:
				return vec2(self.x + float(other[0]), \
				            self.y + float(other[1]))
			except ValueError:
				raise ValueError("the elements of the argument to " + \
				                 "vec2.__add__ must be convertible to float.")

		if (type(other) is list) or (type(other) is tuple):
			raise ValueError("Expected the argument of vec2.__add__ to " + \
			                 "have length 2, instead got %d" % len(other))
		else:
			raise TypeError("unsupported operand type for vec2 + " + \
		                    str(type(other)))

	def __sub__(self, other):
		"""Implementation of the binary -.  Works with other vec2s, as well as
		lists and tuples of length 2."""
		if is_vec2_friendly(other):
			try:
				return vec2(self.x - float(other[0]), \
				            self.y - float(other[1]))
			except ValueError:
				raise ValueError("the elements of the argument to " + \
				                 "vec2.__sub__ must be convertible to float.")

		if (type(other) is list) or (type(other) is tuple):
			raise ValueError("Expected the argument of vec2.__sub__ to " + \
			                 "have length 2, instead got %d" % len(other))
		else:
			raise TypeError("unsupported operand type for vec2 - " + \
		                    str(type(other)))
		
	def __mul__(self, other):
		"""Implementation of the binary *.  Acts as a dot product between a
		vec2 and another vec2 or list or tuple of length 2.
		However, acts as an elementwise multiplication if the argument is 
		castable to a floating point."""
		if is_vec2_friendly(other):
			return (self.x * other[0] + self.y * other[1])
		try:
			scalar = float(other)
			return vec2(self.x * scalar, \
			            self.y * scalar)
		except:
			pass
		raise TypeError("unsupported operand type for vec2 * " + \
		                str(type(other)))

	def __div__(self, other):
		"""Implementation of the binary /.  Acts as an elementwise division
		if the argument is castable to a floating point."""
		try:
			scalar = float(other)
			if abs(scalar) < 1e-9:
				raise ZeroDivisionError("vec2s may not be divided by zero")
			return vec2(self.x / scalar, \
			            self.y / scalar)
		except:
			pass
		raise TypeError("unsupported operand type for vec2 / " + \
		                str(type(other)))

	def __lt__(self, other):
		"""Implementation of the binary < operator.  Returns True if this
		vector is shorter (has inferior magnitude) to another vec2, or if it
		has magnitude less than a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec2):
			return self.mag() < other.mag()
		try:
			return self.mag() < float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec2 < " + \
		                str(type(other)))

	def __le__(self, other):
		"""Implementation of the binary <= operator.  Returns True if this
		vector is shorter than or the same length as another vec2, or if it
		has magnitude less than or equal to a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec2):
			return self.mag() <= other.mag()
		try:
			return self.mag() <= float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec2 <= " + \
		                str(type(other)))

	def __gt__(self, other):
		"""Implementation of the binary > operator.  Returns True if this
		vector is longer (has greater magnitude) to another vec2, or if it
		has magnitude greater than a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec2):
			return self.mag() > other.mag()
		try:
			return self.mag() > float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec2 > " + \
		                str(type(other)))

	def __ge__(self, other):
		"""Implementation of the binary >= operator.  Returns True if this
		vector is at least as long as another vec2, or if it
		has magnitude greater than or equal to a value convertible to float.
		CURRENTLY DOES NOT COMPARE TO LISTS OR TUPLES (laziness)"""
		if isinstance(other, vec2):
			return self.mag() >= other.mag()
		try:
			return self.mag() >= float(other)
		except TypeError:
			pass
		raise TypeError("unsupported operand type for vec2 >= " + \
		                str(type(other)))

	def d2(self):
		"""distance-squared, for use in mag and other shortcuttery"""
		return (self.x*self.x + self.y*self.y)

	def mag(self):
		"""Returns the magnitude of this vector, that is, the 2-norm."""
		return self.d2()**.5

	def normalize(self):
		"""Returns a copy of this vector that has been normalized, that is,
		a vec2 with the same direction, but magnitude 1.
		The exception to this is a vector of magnitude zero, which cannot be
		normalized.  Then it will return the zero vector."""
		if self:
			mag = self.mag()
			return vec2(self.x / mag, \
			            self.y / mag)
		else:
			return vec2(0, 0)
			
	def min(self, other):
		"""Given another vec2-friendly, returns the element-wise minimum of the
		two vectors."""
		if not is_vec2_friendly(other):
			raise ValueError("Expected argument to be vec2-friendly, " + \
			                 "instead got %s" % other)
		return vec2(min(self[0], other[0]), \
		            min(self[1], other[1]))

	def max(self, other):
		"""Given another vec2-friendly, returns the element-wise maximum of the
		two vectors."""
		if not is_vec2_friendly(other):
			raise ValueError("Expected argument to be vec2-friendly, " + \
			                 "instead got %s" % other)
		return vec2(max(self[0], other[0]), \
		            max(self[1], other[1]))

	def random(self):
		"""Returns a vector chosen at random from within the box (0,0,0), this
		vector. The randomness is a uniform distribution.
		TODO: consider adding other randomizations."""
		return vec2(self.x * random(), \
		            self.y * random())

	def int(self):
		"""Returns a copy of the vector with its elements cast as ints.
		Gives up if it proves difficult on a particular element."""
		temp = vec2(self)
		for i in range(2):
			try:
				temp[i] = int(temp[i])
			except:
				pass
		return temp
	
	def float(self):
		"""Returns a copy of the vector with its elements cast as floats.
		Gives up if it proves difficult on a particular element."""
		temp = vec2(self)
		for i in range(2):
			try:
				temp[i] = float(temp[i])
			except:
				pass
		return temp

	def angle(self, other, mode="rad"):
		"""The angle between two vectors is the arccosine of their dot product
		devided by the product of their magnitudes."""
		if not is_vec2_friendly(other):
			raise ValueError("Expected argument to be vec2-friendly, " + \
			                 "instead got %s" % other)
		temp = vec2(other)
		if mode == "rad":
			return acos(self.normalize() * temp.normalize())
		if mode == "deg":
			return acos(self.normalize() * temp.normalize()) * 180 / pi
		else:
			raise ValueError("mode must be 'deg' or 'rad', got %s" % mode)